Skip to content

Encrypt payment_metadata when we build the payment secret#4628

Merged
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally
May 22, 2026
Merged

Encrypt payment_metadata when we build the payment secret#4628
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally

Conversation

@TheBlueMatt
Copy link
Copy Markdown
Collaborator

In 657ac8f we started committing to the payment_metadata in the payment_secret. We'd largely assumed that downstream code could simply encrypt the payment_metadata itself before passing it to lightning and decrypt before reading it from lightning. However, this presents a challenge - we'd very much love for that downstream code to avoid adding any extra bytes to its payment_metadata if at all possible, but it doesn't have a great way to get a decent IV without simply shoving it in the encrypted payment_metadata.

Instead, here, we encrypt and decrypt the payment_metadata internally in lightning. This allows us to reuse the IV that is used for lightning-generated payment_hashes as the IV for the encrypted payment_metadata as well. Sadly, we don't have any similar IV for user-provided payment_hashes. In that case, we simply accept the limitations and document that users must avoid encrypting multiple payment_metadatas for payments with the same payment_hash. This avoids padding the size of the payment_metadata and should generally not be a material concern - payment_hash reuse should generally not exist anyway, and if it does it should only be in cases where its "the same payment" being retried after failure, at which point payment_metadata should hopefully be the same.

@TheBlueMatt TheBlueMatt requested a review from tnull May 20, 2026 20:50
@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented May 20, 2026

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@TheBlueMatt TheBlueMatt requested a review from joostjager May 20, 2026 20:50
@TheBlueMatt TheBlueMatt added this to the 0.3 milestone May 20, 2026
Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment thread lightning/src/ln/channelmanager.rs Outdated
@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

ldk-claude-review-bot commented May 20, 2026

I've thoroughly re-reviewed the entire PR diff, reading all key files. Let me check the memory for any prior issues that might now be resolved or still pending.

The diff is clean. The prior review already captured the meaningful issues, and I verified:

  • Encrypt-then-MAC ordering is correct for both LdkPaymentHash and UserPaymentHash paths
  • HMAC inputs during create/create_from_hash match those in verify (including the metadata length commitment and the IV for UserPaymentHash)
  • ChaCha20 key/nonce/counter extraction in apply_chacha20 is consistent with prior inline usage
  • In-place decryption via Option<&mut Vec<u8>> correctly propagates decrypted metadata to PaymentClaimable events
  • Borrow patterns (e.g., payment_metadata.as_deref().map(Vec::as_slice) before later if let Some(metadata) = payment_metadata) are sound
  • HKDF 8-key expansion follows RFC 5869
  • New do_payment_metadata_end_to_end test covers all three creation paths with encryption round-trip assertions
  • SpontaneousPayment metadata rejection is a correct hardening
  • BOLT 12 and phantom invoice paths correctly pass None for metadata

No new issues found beyond the prior review.

Review Summary

No new issues found beyond those flagged in the prior review pass.

Prior comment status

  • inbound_payment.rs:232 (undocumented 16-byte overhead) — Still valid. create_from_hash appends a 16-byte IV to the encrypted metadata but this is not mentioned in the create_inbound_payment_for_hash docs.
  • payment_tests.rs:1541 (stale function name) — Still valid but outside diff hunk range.
  • channelmanager.rs:15089 (doc typo) — Resolved in current state.
  • channelmanager.rs:14526 (stale comment) — Resolved.
  • max_payment_path_len_tests.rs:109 (IV overflow) — No longer applicable; test switched to create_inbound_payment (same-length encryption).

Verification notes

  • Crypto correctness verified: encrypt-then-MAC ordering, HMAC input consistency, ChaCha20 parameter extraction, nonce uniqueness, metadata length commitment.
  • Edge cases verified: empty metadata (Some(vec![]) vs None), missing metadata on send, extra metadata on send — all correctly rejected by HMAC.
  • In-place decryption correctly strips the appended IV for UserPaymentHash and preserves length for LdkPaymentHash.
  • No timing side channels introduced beyond the pre-existing method-type leakage noted in existing comments.

Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment thread lightning/src/ln/inbound_payment.rs Outdated
}

if let Some(metadata) = payment_metadata {
ChaCha20::new_from_block(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following the PaymentMetadata / EncryptedPaymentMetadata state pattern we introduced in lightningdevkit/ldk-node#899?

We intentionally did that to improve readability and to use the type system to ensure we can't ever leak an unencrypted raw Vec<u8> into the metadata field.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not seeing much opportunity to do this. lightning-invoice can't switch types as it has to handle counterparty data, so we have to make it a Vec<u8> again almost immediately. We could do it in PendingHTLCRouting::Receive/ReceiveKeysend but the structure in process_receive_htlcs is a bit annoying and I'm not entirely clear its worth it just for the inbound edge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, okay, I think I would still prefer a bit more structured/typed approach but at the very least it would be good to make this some dedicated utility methods, if only to isolate all the unwraps in a single place rather than sprinkling them everywhere (and reviewers getting used to reading "unwrap").

Comment thread lightning/src/ln/inbound_payment.rs Outdated
Comment thread lightning/src/ln/inbound_payment.rs Outdated
Comment thread lightning/src/ln/inbound_payment.rs Outdated
Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment thread lightning/src/ln/max_payment_path_len_tests.rs
Comment thread lightning/src/ln/inbound_payment.rs
Copy link
Copy Markdown
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is very failing right now.

@TheBlueMatt TheBlueMatt requested review from joostjager and tnull May 21, 2026 18:59
@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

❌ Patch coverage is 91.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.69%. Comparing base (1743b99) to head (4fac0fe).
⚠️ Report is 16 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/inbound_payment.rs 91.22% 5 Missing ⚠️
lightning/src/ln/channelmanager.rs 83.33% 2 Missing ⚠️
lightning/src/ln/invoice_utils.rs 92.85% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4628      +/-   ##
==========================================
+ Coverage   86.58%   86.69%   +0.10%     
==========================================
  Files         159      159              
  Lines      110498   110604     +106     
  Branches   110498   110604     +106     
==========================================
+ Hits        95678    95888     +210     
+ Misses      12281    12198      -83     
+ Partials     2539     2518      -21     
Flag Coverage Δ
fuzzing-fake-hashes 7.02% <24.13%> (+0.40%) ⬆️
fuzzing-real-hashes 23.28% <29.88%> (+0.13%) ⬆️
tests 86.25% <91.66%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

joostjager
joostjager previously approved these changes May 22, 2026
Copy link
Copy Markdown
Contributor

@joostjager joostjager left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, aside from rustfmt failing

Copy link
Copy Markdown
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash. rustfmt and bench are unhappy, but otherwise fine by me!

In 657ac8f we started committing
to the `payment_metadata` in the `payment_secret`. We'd largely
assumed that downstream code could simply encrypt the
`payment_metadata` itself before passing it to `lightning` and
decrypt before reading it from `lightning`. However, this presents
a challenge - we'd very much love for that downstream code to avoid
adding any extra bytes to its `payment_metadata` if at all
possible, but it doesn't have a great way to get a decent IV
without simply shoving it in the encrypted `payment_metadata`.

Instead, here, we encrypt and decrypt the `payment_metadata`
internally in `lightning`. This allows us to reuse the IV that is
used for `lightning`-generated `payment_hash`es as the IV for the
encrypted `payment_metadata` as well. Sadly, we don't have any
similar IV for user-provided `payment_hash`es. In that case, we
simply accept the limitations and document that users must avoid
encrypting multiple `payment_metadata`s for payments with the same
`payment_hash`. This avoids padding the size of the
`payment_metadata` and should generally not be a material concern -
`payment_hash` reuse should generally not exist anyway, and if it
does it should only be in cases where its "the same payment" being
retried after failure, at which point `payment_metadata` should
hopefully be the same.
Most of our `chacha20` calls don't actually care about the concept
of ChaCha20's "seek" vs "nonce" - we just want to use the full
128 bits of nonce space as nonce. Here we unify those calls to
keep a consistent API and consolidate the `unwrap`s to one place.
@TheBlueMatt TheBlueMatt force-pushed the 2026-05-encrypt-metadata-internally branch from cfbf274 to 4fac0fe Compare May 22, 2026 16:44
@TheBlueMatt
Copy link
Copy Markdown
Collaborator Author

Fixed and squashed:

$ git diff-tree -U1 cfbf274d56 4fac0fe1c1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index a0da238311..2adb0a1ca5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -22134,3 +22134,4 @@ pub mod bench {
 				let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array());
-				let payment_secret = $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None, None).unwrap();
+				let (payment_secret, _no_payment_metadata) =
+					$node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None, None).unwrap();

diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 8e682baa43..3adc638029 100644
--- a/lightning/src/sign/mod.rs
+++ b/lightning/src/sign/mod.rs
@@ -40,3 +40,3 @@ use lightning_invoice::RawBolt11Invoice;
 use crate::chain::transaction::OutPoint;
-use crate::crypto::utils::{apply_chacha20 ,hkdf_extract_expand_twice, sign, sign_with_aux_rand};
+use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand};
 use crate::ln::chan_utils;

@TheBlueMatt TheBlueMatt requested review from joostjager and tnull May 22, 2026 16:45
@TheBlueMatt
Copy link
Copy Markdown
Collaborator Author

No material changes since @tnull said "otherwise fine by me", so just gonna land.

@TheBlueMatt TheBlueMatt merged commit b7f58cd into lightningdevkit:main May 22, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants